package org.apache.solr.util;

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.lucene.analysis.util.ResourceLoader;

import org.xml.sax.InputSource;
import org.xml.sax.EntityResolver;
import org.xml.sax.ext.EntityResolver2;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.URIResolver;
import javax.xml.transform.sax.SAXSource;
import javax.xml.stream.XMLResolver;
import javax.xml.stream.XMLStreamException;

/**
 * This is a helper class to support resolving of XIncludes or other hrefs
 * inside XML files on top of a {@link ResourceLoader}. Just plug this class on
 * top of a {@link ResourceLoader} and pass it as {@link EntityResolver} to SAX
 * parsers or via wrapper methods as {@link URIResolver} to XSL transformers or
 * {@link XMLResolver} to STAX parsers. The resolver handles special SystemIds
 * with an URI scheme of {@code solrres:} that point to resources. To produce
 * such systemIds when you initially call the parser, use
 * {@link #createSystemIdFromResourceName} which produces a SystemId that can be
 * included along the InputStream coming from
 * {@link ResourceLoader#openResource}.
 * <p>
 * In general create the {@link InputSource} to be passed to the parser like:
 * </p>
 * 
 * <pre class="prettyprint">
 * InputSource is = new InputSource(loader.openSchema(name));
 * is.setSystemId(SystemIdResolver.createSystemIdFromResourceName(name));
 * final DocumentBuilder db = DocumentBuilderFactory.newInstance()
 * 		.newDocumentBuilder();
 * db.setEntityResolver(new SystemIdResolver(loader));
 * Document doc = db.parse(is);
 * </pre>
 */
public final class SystemIdResolver implements EntityResolver, EntityResolver2 {
	private static final Logger log = LoggerFactory
			.getLogger(SystemIdResolver.class);

	public static final String RESOURCE_LOADER_URI_SCHEME = "solrres";
	public static final String RESOURCE_LOADER_AUTHORITY_ABSOLUTE = "@";

	private final ResourceLoader loader;

	public SystemIdResolver(ResourceLoader loader) {
		this.loader = loader;
	}

	public EntityResolver asEntityResolver() {
		return this;
	}

	public URIResolver asURIResolver() {
		return new URIResolver() {
			public Source resolve(String href, String base)
					throws TransformerException {
				try {
					final InputSource src = SystemIdResolver.this
							.resolveEntity(null, null, base, href);
					return (src == null) ? null : new SAXSource(src);
				} catch (IOException ioe) {
					throw new TransformerException("Cannot resolve entity", ioe);
				}
			}
		};
	}

	public XMLResolver asXMLResolver() {
		return new XMLResolver() {
			public Object resolveEntity(String publicId, String systemId,
					String baseURI, String namespace) throws XMLStreamException {
				try {
					final InputSource src = SystemIdResolver.this
							.resolveEntity(null, publicId, baseURI, systemId);
					return (src == null) ? null : src.getByteStream();
				} catch (IOException ioe) {
					throw new XMLStreamException("Cannot resolve entity", ioe);
				}
			}
		};
	}

	URI resolveRelativeURI(String baseURI, String systemId)
			throws URISyntaxException {
		URI uri;

		// special case for backwards compatibility: if relative systemId starts
		// with "/" (we convert that to an absolute solrres:-URI)
		if (systemId.startsWith("/")) {
			uri = new URI(RESOURCE_LOADER_URI_SCHEME,
					RESOURCE_LOADER_AUTHORITY_ABSOLUTE, "/", null, null)
					.resolve(systemId);
		} else {
			// simply parse as URI
			uri = new URI(systemId);
		}

		// do relative resolving
		if (baseURI != null) {
			uri = new URI(baseURI).resolve(uri);
		}

		return uri;
	}

	// *** EntityResolver(2) methods:

	public InputSource getExternalSubset(String name, String baseURI) {
		return null;
	}

	public InputSource resolveEntity(String name, String publicId,
			String baseURI, String systemId) throws IOException {
		if (systemId == null)
			return null;
		try {
			final URI uri = resolveRelativeURI(baseURI, systemId);

			// check schema and resolve with ResourceLoader
			if (RESOURCE_LOADER_URI_SCHEME.equals(uri.getScheme())) {
				String path = uri.getPath(), authority = uri.getAuthority();
				if (!RESOURCE_LOADER_AUTHORITY_ABSOLUTE.equals(authority)) {
					path = path.substring(1);
				}
				try {
					final InputSource is = new InputSource(
							loader.openResource(path));
					is.setSystemId(uri.toASCIIString());
					is.setPublicId(publicId);
					return is;
				} catch (RuntimeException re) {
					// unfortunately XInclude fallback only works with
					// IOException, but openResource() never throws that one
					throw (IOException) (new IOException(re.getMessage())
							.initCause(re));
				}
			} else {
				// resolve all other URIs using the standard resolver
				return null;
			}
		} catch (URISyntaxException use) {
			log.warn(
					"An URI systax problem occurred during resolving SystemId, falling back to default resolver",
					use);
			return null;
		}
	}

	public InputSource resolveEntity(String publicId, String systemId)
			throws IOException {
		return resolveEntity(null, publicId, null, systemId);
	}

	public static String createSystemIdFromResourceName(String name) {
		name = name.replace(File.separatorChar, '/');
		final String authority;
		if (name.startsWith("/")) {
			// a hack to preserve absolute filenames and keep them absolute
			// after resolving, we set the URI's authority to "@" on absolute
			// filenames:
			authority = RESOURCE_LOADER_AUTHORITY_ABSOLUTE;
		} else {
			authority = null;
			name = "/" + name;
		}
		try {
			return new URI(RESOURCE_LOADER_URI_SCHEME, authority, name, null,
					null).toASCIIString();
		} catch (URISyntaxException use) {
			throw new IllegalArgumentException(
					"Invalid syntax of Solr Resource URI", use);
		}
	}

}
